msg_tool\scripts\bgi/
bp.rs

1//! Buriko General Interpreter/Ethornell BP Script (._bp)
2use crate::ext::io::*;
3use crate::scripts::base::*;
4use crate::types::*;
5use crate::utils::encoding::{decode_to_string, encode_string};
6use anyhow::Result;
7use std::io::{Seek, SeekFrom};
8
9#[derive(Debug)]
10/// Builder for BGI BP scripts.
11pub struct BGIBpScriptBuilder {}
12
13impl BGIBpScriptBuilder {
14    /// Creates a new instance of `BGIBpScriptBuilder`.
15    pub fn new() -> Self {
16        BGIBpScriptBuilder {}
17    }
18}
19
20impl ScriptBuilder for BGIBpScriptBuilder {
21    fn default_encoding(&self) -> Encoding {
22        Encoding::Cp932
23    }
24
25    fn build_script(
26        &self,
27        buf: Vec<u8>,
28        _filename: &str,
29        encoding: Encoding,
30        _archive_encoding: Encoding,
31        config: &ExtraConfig,
32        _archive: Option<&Box<dyn Script>>,
33    ) -> Result<Box<dyn Script>> {
34        Ok(Box::new(BGIBpScript::new(buf, encoding, config)?))
35    }
36
37    fn extensions(&self) -> &'static [&'static str] {
38        &["_bp"]
39    }
40
41    fn script_type(&self) -> &'static ScriptType {
42        &ScriptType::BGIBp
43    }
44}
45
46#[derive(Debug)]
47struct BpString {
48    offset_pos: usize,
49    text_offset: u16,
50}
51
52#[derive(Debug)]
53/// BGI BP script.
54pub struct BGIBpScript {
55    data: MemReader,
56    header_size: u32,
57    strings: Vec<BpString>,
58    encoding: Encoding,
59}
60
61impl BGIBpScript {
62    /// Creates a new instance of `BGIBpScript` from a buffer.
63    ///
64    /// * `buf` - The buffer containing the script data.
65    /// * `encoding` - The encoding of the script.
66    /// * `config` - Extra configuration options.
67    pub fn new(buf: Vec<u8>, encoding: Encoding, _config: &ExtraConfig) -> Result<Self> {
68        let mut reader = MemReader::new(buf);
69        let header_size = reader.read_u32()?;
70        let instr_size = reader.read_u32()?;
71        if header_size as usize + instr_size as usize != reader.data.len() {
72            return Err(anyhow::anyhow!("Invalid bp script file size"));
73        }
74        let mut last_instr_pos = 0;
75        reader.seek(SeekFrom::Start(header_size as u64))?;
76        let max_instr_len = reader.data.len();
77        while reader.pos < max_instr_len {
78            let instr = reader.peek_u8()?;
79            if instr == 0x17 {
80                let need_zero_bytes = 3 - reader.pos % 4;
81                let mut all_zero = true;
82                for i in 0..need_zero_bytes {
83                    let b = reader.peek_u8_at(reader.pos as u64 + 1 + i as u64)?;
84                    if b != 0 {
85                        all_zero = false;
86                        break;
87                    }
88                }
89                if all_zero {
90                    last_instr_pos = reader.pos;
91                    reader.pos += 1 + need_zero_bytes;
92                    continue;
93                }
94            }
95            reader.pos += 1;
96        }
97        if last_instr_pos == 0 {
98            return Err(anyhow::anyhow!("No end instruction found in bp script"));
99        }
100        reader.seek(SeekFrom::Start(header_size as u64))?;
101        let mut strings = Vec::new();
102        while reader.pos < last_instr_pos {
103            let ins = reader.read_u8()?;
104            if ins == 5 {
105                let text_offset = reader.peek_u16()?;
106                let text_address = reader.pos + text_offset as usize - 1;
107                if text_address >= last_instr_pos
108                    && text_address < reader.data.len()
109                    && (text_address <= last_instr_pos + 1 || reader.data[text_address - 1] == 0)
110                {
111                    strings.push(BpString {
112                        offset_pos: reader.pos,
113                        text_offset,
114                    });
115                    reader.pos += 2;
116                }
117            }
118        }
119        return Ok(BGIBpScript {
120            data: reader,
121            header_size,
122            strings,
123            encoding,
124        });
125    }
126}
127
128impl Script for BGIBpScript {
129    fn default_output_script_type(&self) -> OutputScriptType {
130        OutputScriptType::Json
131    }
132
133    fn default_format_type(&self) -> FormatOptions {
134        FormatOptions::None
135    }
136
137    fn extract_messages(&self) -> Result<Vec<Message>> {
138        let mut messages = Vec::new();
139        for i in self.strings.iter() {
140            let text_address = i.offset_pos + i.text_offset as usize - 1;
141            // println!("offset: {}, text address: {}, text_offset: {}", i.offset_pos, text_address, i.text_offset);
142            let str = self.data.cpeek_cstring_at(text_address as u64)?;
143            let str = decode_to_string(self.encoding, str.as_bytes(), true)?;
144            messages.push(Message {
145                name: None,
146                message: str,
147            });
148        }
149        Ok(messages)
150    }
151
152    fn import_messages<'a>(
153        &'a self,
154        messages: Vec<Message>,
155        mut file: Box<dyn WriteSeek + 'a>,
156        _filename: &str,
157        encoding: Encoding,
158        replacement: Option<&'a ReplacementTable>,
159    ) -> Result<()> {
160        if messages.len() != self.strings.len() {
161            return Err(anyhow::anyhow!(
162                "Number of messages does not match the number of strings in the script"
163            ));
164        }
165        file.write_all(&self.data.data)?;
166        let mut new_pos = self.data.data.len();
167        for (i, mes) in self.strings.iter().zip(messages) {
168            let text_address = i.offset_pos + i.text_offset as usize - 1;
169            let old_str_len = self
170                .data
171                .cpeek_cstring_at(text_address as u64)?
172                .as_bytes_with_nul()
173                .len();
174            let mut str = mes.message;
175            if let Some(replacement) = replacement {
176                for (key, value) in replacement.map.iter() {
177                    str = str.replace(key, value);
178                }
179            }
180            let mut str = encode_string(encoding, &str, false)?;
181            str.push(0); // Null terminator
182            let new_str_len = str.len();
183            if new_str_len > old_str_len {
184                file.write_all(&str)?;
185                let new_text_offset = (new_pos - i.offset_pos + 1) as u16;
186                file.write_u16_at(i.offset_pos as u64, new_text_offset)?;
187                new_pos += new_str_len;
188            } else {
189                file.write_all_at(text_address as u64, &str)?;
190            }
191        }
192        let new_instr_size = (new_pos - self.header_size as usize) as u32;
193        file.write_u32_at(4, new_instr_size)?;
194        Ok(())
195    }
196}